Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6장. 아키텍처 관점에서의 DB 접근 계층

지금까지 데이터베이스 접근 방식의 흐름을 살펴보았다.

  • Raw Query
  • Query Builder
  • ORM

이제 중요한 것은
👉 이들을 “어떻게 코드 구조 안에 배치할 것인가”이다.


1. 왜 구조가 필요한가

서비스 규모가 커질수록 데이터 접근 코드는 빠르게 증가한다.

이 과정에서 자연스럽게 다음과 같은 문제가 발생한다.

  • 쿼리가 여러 곳에 흩어진다
  • 동일한 로직이 반복된다
  • 수정 시 영향 범위를 파악하기 어렵다

👉 즉, 데이터 접근을 체계적으로 관리할 구조가 필요하다


2. 데이터 접근 계층 구조

일반적으로 데이터 접근은 다음과 같은 계층 구조로 구성된다.

[ Controller ]
      ↓
[  Service  ]
      ↓
[ Repository ]
      ↓
[ Database ]

각 계층은 명확한 역할을 가진다.


3. 계층별 역할

Controller

app.get('/users/:id', async (req, res) => {
  const user = await userService.getUser(req.params.id);
  res.json(user);
});
  • 요청/응답 처리
  • 비즈니스 로직 없음

Service

class UserService {
  async getUser(userId: number) {
    return await userRepository.findById(userId);
  }
}
  • 비즈니스 로직 담당
  • 데이터 접근 방식에 의존하지 않음

Repository

class UserRepository {
  async findById(userId: number) {
    return await prisma.user.findUnique({
      where: { id: userId }
    });
  }
}
  • 데이터 접근 전담
  • ORM / Query Builder / Raw Query 사용

Database

  • 실제 데이터 저장소
  • MySQL, PostgreSQL 등

4. 데이터 흐름

계층 간 데이터는 역할에 맞게 형태가 달라진다.

Repository → Entity

type UserEntity = {
  id: number;
  email: string;
  name: string;
};

👉 데이터베이스 구조 그대로 표현

Service → DTO 변환

type UserDTO = {
  id: number;
  name: string;
};
class UserService {
  async getUser(userId: number): Promise<UserDTO> {
    const user = await userRepository.findById(userId);

    return {
      id: user.id,
      name: user.name
    };
  }
}

👉 외부에 필요한 데이터만 전달


5. 이 구조가 가지는 장점

이 구조는 단순한 계층 분리가 아니라
👉 실제 개발 과정에서 발생하는 문제를 해결하기 위한 설계 방식이다.

1) 유지보수성 – 변경 영향 최소화

❌ 구조가 없는 경우

app.get('/users/:id', async (req, res) => {
  const user = await db.query("SELECT * FROM users WHERE id = 1");
  res.json(user);
});

👉 문제:

  • SQL이 여러 곳에 퍼짐
  • 테이블 변경 시 전부 수정

⭕ 구조가 있는 경우

class UserRepository {
  async findById(id: number) {
    return db.query("SELECT * FROM users WHERE id = ?", [id]);
  }
}

👉 변경 발생:

// users → members 변경
return db.query("SELECT * FROM members WHERE id = ?", [id]);

👉 Repository만 수정하면 끝

👉 핵심:
변경이 한 곳에 집중된다


2) 테스트 용이성 – DB 없이 테스트

❌ 구조가 없는 경우

class UserService {
  async getUser(id: number) {
    return db.query("SELECT * FROM users WHERE id = ?", [id]);
  }
}

👉 문제:

  • DB 필요
  • 테스트 느림

⭕ 구조가 있는 경우

interface UserRepository {
  findById(id: number): Promise<UserEntity>;
}
class MockUserRepository implements UserRepository {
  async findById(id: number) {
    return { id, name: "test-user", email: "test@test.com" };
  }
}
const service = new UserService(new MockUserRepository());
const user = await service.getUser(1);

👉 DB 없이 테스트 가능

👉 핵심:
비즈니스 로직을 독립적으로 검증할 수 있다


3) 데이터 보호 – Entity vs DTO

❌ 구조가 없는 경우

res.json(user);

👉 문제:

  • email 등 민감 정보 그대로 노출

⭕ DTO 적용

return {
  id: user.id,
  name: user.name
};

👉 핵심: 외부로 나가는 데이터는 통제된다


4) 기술 변경 유연성

상황

ORM → Raw Query 변경

⭕ 구조가 있는 경우

// 기존
return prisma.user.findUnique(...);

// 변경
return db.query("SELECT * FROM users WHERE id = ?", [id]);

👉 Service / Controller 수정 없음

👉 핵심:
기술 변경이 전체 코드에 영향을 주지 않는다


5) 혼합 전략 적용

class UserRepository {
  async findById(id: number) {
    return prisma.user.findUnique({ where: { id } });
  }

  async searchUsers(...) {
    return knex('users')...;
  }

  async getStatistics() {
    return db.query("SELECT ...");
  }
}

👉 하나의 구조 안에서

  • ORM
  • Query Builder
  • Raw Query

공존 가능

👉 핵심:
상황에 맞는 기술 선택이 가능하다


6. 핵심 정리

이 구조의 핵심은 다음과 같다.

  • 데이터 접근은 반드시 분리해야 한다
  • 각 계층은 명확한 역할을 가진다
  • 내부 구현은 자유롭게 변경 가능하다

그리고 가장 중요한 점은

👉 이 구조는 코드를 나누기 위한 것이 아니라
변화에 대응하기 위한 설계다